package com.atguigu.tingshu.common.login.aspect;

import com.alibaba.fastjson.JSONObject;
import com.atguigu.tingshu.common.constant.PublicConstant;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.execption.GuiguException;
import com.atguigu.tingshu.common.login.anotation.NeedLogin;
import com.atguigu.tingshu.common.result.ResultCodeEnum;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Map;

/**
 * Description:
 * 对需要登录的接口进行加强的切面类
 * 在该切面类下，我们会写所有需要对请求进行登录信息认证的加强逻辑
 * ——————即，过滤未登录用户的请求
 * @author Yanxinhao on 2025/1/13
 */
@Aspect
@Component
public class LoginAspect {

    @Autowired
    private StringRedisTemplate redisTemplatel;

    @Around(value = "@annotation(com.atguigu.tingshu.common.login.anotation.NeedLogin)")
    public Object checkLogin(ProceedingJoinPoint pjp) throws Throwable {
        //1.先获取当前请求中的token信息
        String token = getToken();

        //2.在进行token校验之前，我们先校验@NeedLogin注解的开关是否被启用(required = ture / false)
        boolean needLogin = checkNeedLoginSwitch(pjp);
        //2.1 若开关没有被设置为true，那么我们就不需要去校验它的token(即校验必须登录)
        if (!needLogin && StringUtils.isEmpty(token)){ //同时要求传来的token为空的情况下，才免除校验
            return pjp.proceed();
        }

        //3.校验token信息
        Long userId = checkAccessTokenAndGetUserId(token);
        //4.将userId存入当前ThreadLocal当中
        /**
         * 执行方法的线程，和执行方法时调用这个切面类的线程是同一个线程
         * 所以，在切面类中，我们使用ThreadLocal存储userId，
         * 然后当我们在外层方法中，使用ThreadLocal也可以读取到这个userId
         * 阅读源码，我们能够知道ThreadLocal是和当前线程绑定的
         * 因此在同一个线程执行的过程中，ThreadLocal存储的数据是共享的
         */
        AuthContextHolder.setUserId(userId);
        /**
         * 但是使用ThreadLocal可能会有内存泄露的问题存在
         * 泄露的原因：我们目前是使用线程池调用线程执行任务的，而我们知道线程池中有核心线程数
         * 这里的核心线程数无论如何都不会释放，而是一直处于等待状态，直到有任务来调用线程池
         * 而我们的ThreadLocal和单个线程之间的关系是绑定的，若线程不销毁
         * 那么ThreadLocal中的数据也不会被垃圾回收器释放，因此会造成内存泄露
         * ps：若使用new Thread() new一个线程来执行任务，执行完毕后，这个线程就会被GC回收
         *     因此这样执行任务的时候，ThreadLocal就会随着线程而销毁，不会造成内存泄露
         */
        //综上所述！我们最终在执行完毕的时候，一定要调用ThreadLocal的remove方法，来释放ThreadLocal中的数据
        Object result;
        try {
            result = pjp.proceed();
        } finally {
            AuthContextHolder.removeUserId();
        }
        return result;
    }

    private boolean checkNeedLoginSwitch(ProceedingJoinPoint pjp) {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        NeedLogin annotation = signature.getMethod().getAnnotation(NeedLogin.class);
        return annotation.required();
    }


    public Long checkAccessTokenAndGetUserId(String token){
        //1.先判断当前token是否为空
        if (StringUtils.isEmpty(token)){
            throw new GuiguException(ResultCodeEnum.LOGIN_AUTH);
        }
        /**
         * 公钥用于加密和验签
         * 私钥用于解密和加签
         */
        //2.对token进行验签，获取JWT对象
        Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(PublicConstant.PUBLIC_KEY));
        //3.针对JWT对象，获取载荷信息
        String claims = jwt.getClaims();
        Map map = JSONObject.parseObject(claims.toString(), Map.class);
        //4.从map中尝试获取openId和userId
        String openId = (String) map.get("openId");
        Object userId = map.get("userId");
        //5.有了openId，我们现在就能去redis数据库中查询对应的存储的token是否和请求携带的token一致
        String accessTokenKey = RedisConstant.USER_LOGIN_KEY_PREFIX + openId;
        String accessTokenFromRedis = redisTemplatel.opsForValue().get(accessTokenKey);
        if (StringUtils.isEmpty(accessTokenFromRedis) || !token.equals(accessTokenFromRedis)){
            throw new GuiguException(401,"token已全部过期");
        }


        return Long.parseLong(userId+ "");
    }

    /**
     * 根据当前请求，获取token信息
     * @return
     */
    public String getToken(){
        //1.获取与当前线程绑定的HTTP请求属性的对象
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        //2.根据请求的属性对象，尝试获取请求对象
        HttpServletRequest request = requestAttributes.getRequest();

        //3.根据请求对象，获取请求头中的数据
        String refreshToken = request.getHeader("token");

        return refreshToken;
    }
}